﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using System.Net;
using System.Net.Sockets;
using Models;
using Microsoft.Xna.Framework;

namespace Server
{
    public class Server
    {
        #region Fields
        // Queue
        private Queue<QueueItem> commandQueue;
        
        // Client data
        private Dictionary<IPEndPoint, Client> clients;
        const int MAX_Clients = 1;
        const long TIMEOUT_VALUE = 100000000; //ticks (100 nano secs)

        //timer to run heartbeat;
        private Timer heartBeat;
        const int TIMER_INTERVAL = 10;
        private long lastNow = DateTime.Now.Ticks;

        // UDP server
        private UdpClient server;
        const int RECEIVE_PORT = 5000;
        const int SPRITE_OBJECTS_PER_UPDATE_PACKET = 24;
        // UDP Header format
        // [uint Protocol id]
        // [uint sequence number]
        const int UDP_HEADER_BYTE_LENGTH = 8;
        const string PROTOCOL_ID = "GAME";


        // look and feel of server
        const ConsoleColor FOREGROUND = ConsoleColor.Green;
        const ConsoleColor BACKGROUND = ConsoleColor.DarkGreen;
        const string PROMPT = "> ";
        bool exiting = false;

        // game related fields
        const float LASER_SECONDS_TO_LIVE = 1f;
        private Dictionary<int,Sprite> sprites;
        private int windowWidth = 0;
        private int windowHeight = 0;
        private Dictionary<int, List<Sprite>> spritesByClass;
        private List<int> deleteingSprites;
        
        private int score;
        private byte lives;
        #endregion

        #region Constructors

        public Server(Queue<QueueItem> queue)
        {
            this.commandQueue = new Queue<QueueItem>();
            
            // Initiate Dictionaries
            clients = new Dictionary<IPEndPoint, Client>();
            sprites = new Dictionary<int, Sprite>();
            spritesByClass = new Dictionary<int, List<Sprite>>();
            for (int i = 0; i < 6; i++)
            {
                spritesByClass.Add(i, new List<Sprite>());
            }
            deleteingSprites = new List<int>();
            
            // Print welcome message
            printWelcome();

            // Start heartbeat timer
            heartBeat = new Timer(runTimer, 0, 0, TIMER_INTERVAL);

            // Start UDP listener
            server = new UdpClient(RECEIVE_PORT);
            Thread UdpThread = new Thread(() => waitForPackets(server));
            UdpThread.Start();
        }
        #endregion

        #region Public Methods

        public void Run()
        {
            string reply = "";
            ConsoleKeyInfo k = new ConsoleKeyInfo();
            printLine("Type 'exit' to exit!");
            while ((reply != "exit" && k.Key != ConsoleKey.Enter) && !exiting)
            {               
                while (!Console.KeyAvailable && !exiting)
                {
                    Thread.Sleep(250);
                }
                if (Console.KeyAvailable)
                {
                    k = Console.ReadKey();
                    if (k.Key != ConsoleKey.Enter)
                    {
                        reply += k.Key;
                    }
                } 
            }
        }
        #endregion

        #region Private Methods

        private void runTimer(Object state)
        {
            // How long since last run of timer
            long now = DateTime.Now.Ticks;
            TimeSpan timeElapsed = new TimeSpan(now - lastNow);
            lastNow = now;

            // Is there any items in queue? Process
            while (commandQueue.Count > 0)
            {
                processQueue(commandQueue.Dequeue(), now, (float)timeElapsed.TotalMilliseconds);
            }

            // Advance time in sprites
            List<int> destroyList = new List<int>();
            foreach (KeyValuePair<int, Sprite> item in sprites)
            {
                item.Value.Update(timeElapsed);
                if (item.Value.DestructionTime())
                {
                    destroyList.Add(item.Key);
                }
            }
            foreach (int item in destroyList)
            {
                sendDestroyObject(item);
            }

            //Collision Detection 

            // laser haver hit meteor?
            foreach (Sprite laser in spritesByClass[5])
            {
                for (int i = 1; i < 5; i++ )
                    collideLaserToMeteor(i, laser);
            }

            collideShipToMeteor();

            //Check for game end;


            // send sprites to clients
            int numberofPacketsNeeded = (sprites.Count / SPRITE_OBJECTS_PER_UPDATE_PACKET) + 1;
            int numberOfSpritesInLastPacket = sprites.Count % SPRITE_OBJECTS_PER_UPDATE_PACKET;
            int index = 0;
            bool isThisTheLastPacket;
            int numberOfPackets = 0;
            byte[] data = null;
            foreach (KeyValuePair<int, Sprite> item in sprites)
            {
                if (index % SPRITE_OBJECTS_PER_UPDATE_PACKET == 0)
                {
                    isThisTheLastPacket = (index / SPRITE_OBJECTS_PER_UPDATE_PACKET) + 1 == numberofPacketsNeeded;
                    numberOfPackets = isThisTheLastPacket ? numberOfSpritesInLastPacket : SPRITE_OBJECTS_PER_UPDATE_PACKET;
                    data = new byte[6 + numberOfPackets * 20];
                }
                Buffer.BlockCopy(item.Value.Serialize(item.Key), 0, data, 6 + (index % SPRITE_OBJECTS_PER_UPDATE_PACKET) * 20, 20);
                if (index % SPRITE_OBJECTS_PER_UPDATE_PACKET == numberOfPackets - 1)
                {
                    Buffer.BlockCopy(System.Text.Encoding.ASCII.GetBytes("UPDT"), 0, data, 0, 4);
                    Buffer.BlockCopy(BitConverter.GetBytes((short)numberOfPackets), 0, data, 4, 2);
                    foreach (KeyValuePair<IPEndPoint, Client> client in clients)
                    {
                        sendPacket(data, client.Value);
                    }
                }
                index++;
            }


            // Timeout lost clients
            timeoutClients(now, TIMEOUT_VALUE);

        }

        private void collideLaserToMeteor(int type, Sprite laser)
        {
            List<int> hitList = new List<int>();
            foreach (Sprite meteor in spritesByClass[type])
            {
                if (meteor.PointToSphereCollider(laser.LaunchPoint(0)))
                {
                    int key = sprites.SingleOrDefault(x => x.Value.Equals(meteor)).Key;
                    if (sendDestroyObject(key))
                    {
                        score += (int)Math.Pow(2, type-1) * 10;
                        printLine(score.ToString());
                        key = sprites.SingleOrDefault(x => x.Value.Equals(laser)).Key;
                        sendDestroyObject(key);
                        if (type < 4)
                        {
                            createObject(meteor.RightChild());
                            createObject(meteor.LeftChild());
                        }
                    }
                    
                }
            }
        }
        private void collideShipToMeteorTest()
        {
            if (spritesByClass[0].Count > 0 && spritesByClass[1].Count > 0)
            {
                Sprite ship = spritesByClass[0][0];
                Sprite meteor = spritesByClass[1][0];
                if (ship.SphereToSphereCollider(meteor))
                {
                    Vector2 shipReturn = new Vector2();
                    Vector2 meteorReturn = new Vector2();
                    Vector2 shipToMeteor = ship.CalculateImpulse(meteor, ref shipReturn);
                    Vector2 meteorToShip = meteor.CalculateImpulse(ship, ref meteorReturn);
                    ship.ApplyImpulse(shipReturn + meteorToShip);
                    meteor.ApplyImpulse(meteorReturn + shipToMeteor);
                    Console.WriteLine(shipReturn+ "\n" + shipToMeteor);
                }
            }
        }

        private void collideShipToMeteor()
        {
            if (spritesByClass[0].Count > 0)
            {
                Sprite ship = spritesByClass[0].First();
                List<Sprite> mList = meteors();
                foreach (Sprite meteor in mList)
                {
                    if (ship.SphereToSphereCollider(meteor))
                    {
                        

                    }
                }
            }
            

        }

        private List<Sprite> meteors()
        {
            List<Sprite> meteorList = new List<Sprite>();
            for (int i = 1; i < 5; i++)
            {
                meteorList.AddRange(spritesByClass[i]);
            }
            return meteorList;
        }
        private void waitForPackets(UdpClient server)
        {
            while (true)
            {
                // Initiate variables
                IPEndPoint endpoint = new IPEndPoint(new IPAddress(0), 0);
                string protocolId = ""; ;
                uint sequenceNumber = 0;

                // wait for packet arrival
                byte[] data;
                try
                {
                    data = server.Receive(ref endpoint);
                }
                catch (Exception)
                {
                    data = new byte[1];
                }
                // Check for right packet header length
                bool proceed = data.Length >= UDP_HEADER_BYTE_LENGTH;

                // Process header
                if (proceed)
                {
                    protocolId = System.Text.Encoding.ASCII.GetString(data, 0, 4);
                    //sequenceNumber = BitConverter.ToUInt32(data, 4);

                }

                // Check for protocol Id
                if (proceed)
                {
                    proceed = protocolId == PROTOCOL_ID;
                }

                // Check for client connected
                if (proceed)
                {
                    proceed = clients.Keys.Contains(endpoint);
                    if (!proceed && clients.Count < MAX_Clients)
                    {
                        clients.Add(endpoint, new Client(endpoint, DateTime.Now.Ticks, 0));
                        proceed = true;
                        printLine("Client connected: " + endpoint.ToString());
                    }
                }

                //Check for packet sequence
                if (proceed)
                {
                    proceed = (sequenceNumber > clients[endpoint].RemoteSequenceNumber || sequenceNumber == 0);
                    if (proceed)
                    {
                        clients[endpoint].RemoteSequenceNumber = sequenceNumber;
                    }
                }

                // put packet in queue if valid
                if (proceed)
                {
                    byte[] senddata = new byte[data.Length - UDP_HEADER_BYTE_LENGTH];
                    Buffer.BlockCopy(data, UDP_HEADER_BYTE_LENGTH, senddata, 0, senddata.Length);
                    QueueItem item = new QueueItem(endpoint, senddata);
                    commandQueue.Enqueue(item);
                }
            }
        }

        private void processQueue(QueueItem item, long now, float timeElapsed)
        {
            if (!(item.Payload.Length < 4))
            {
                string command = System.Text.Encoding.ASCII.GetString(item.Payload, 0, 4);
                

                switch (command)
                {
                    case "HELO" :
                        printLine("Command received: " + command);
                        byte[] payload = new byte[4];
                        Buffer.BlockCopy(System.Text.Encoding.ASCII.GetBytes("OLEH"), 0, payload, 0, 4);
                        sendPacket(payload, clients[item.Address]);
                        break;
                    case "PING" :
                        pong(item);
                        break;
                    case "REDY" :
                        printLine("Command received: " + command);
                        windowWidth = BitConverter.ToInt32(item.Payload, 4);
                        windowHeight = BitConverter.ToInt32(item.Payload, 8);
                        printLine(String.Format("Screen resolution set to: {0}x{1}", windowWidth, windowHeight));
                        createInitialWorld(clients[item.Address]);
                        break;
                    case "AERC" :
                        addSprite(item.Payload);
                        break;
                    case "QUIT" :
                        exiting = true;
                        break;
                    case "TURN" :
                        sprites.Values.First().Trottle(new Vector2(BitConverter.ToSingle(item.Payload, 4),BitConverter.ToSingle(item.Payload, 8)),timeElapsed);
                        break;
                    case "FIRE" :
                        fireWeapon();
                        break;
                    case "PORD" :
                        destroySprite(BitConverter.ToInt32(item.Payload,4));
                        break;
                }
                server.Send(item.Payload, item.Payload.Length, item.Address);
                clients[item.Address].LastPacketReceived = now;
            }
        }

        private void destroySprite(int id)
        {
            Sprite sprite;
            if (sprites.TryGetValue(id, out sprite))
            {
                spritesByClass[sprite.Class].Remove(sprite);
                sprites.Remove(id);
                deleteingSprites.Remove(id);
            }
        }

        private void pong(QueueItem data)
        {
            byte[] payload = new byte[17];
            Buffer.BlockCopy(System.Text.Encoding.ASCII.GetBytes("PONG"), 0, payload, 0, 4);
            Buffer.BlockCopy(data.Payload, 4, payload, 4, 8);
            Buffer.BlockCopy(BitConverter.GetBytes(score), 0, payload, 12, 4);
            payload[16] = lives;
            sendPacket(payload, clients[data.Address]);
        }

        private void fireWeapon()
        {
            Vector2 launchPoint = sprites.Values.First().LaunchPoint(-20);
            float rotation = sprites.Values.First().Rotation;
            createObject(createSpriteData(5, launchPoint.X, launchPoint.Y, rotation, 500,0,0,LASER_SECONDS_TO_LIVE));
        }

        private void addSprite(byte[] data)
        {
            int spriteId = BitConverter.ToInt32(data,4); // Sprite ID
            Sprite sprite = new Sprite(
                    BitConverter.ToInt32(data,8),  //textureIndex
                    BitConverter.ToInt32(data,12), //TextureWidth
                    BitConverter.ToInt32(data,16), //TextureHeight
                    BitConverter.ToInt32(data,20), //Colums
                    BitConverter.ToInt32(data,24), //Rows
                    0,
                    new Vector2(BitConverter.ToSingle(data,28), //CenterX
                        BitConverter.ToSingle(data,32)), //CenterY
                    new Vector2(BitConverter.ToSingle(data,36),  //VelocityX
                        BitConverter.ToSingle(data,40)),  //VelocityY
                    BitConverter.ToSingle(data,44), //Offset
                    BitConverter.ToSingle(data, 48), //mass
                    BitConverter.ToSingle(data, 52), //rotationSpeed


                    windowWidth,
                    windowHeight);
            float destructionTimer = BitConverter.ToSingle(data, 56); //destructionTimer
            if (destructionTimer > 0)
            {
                sprite.SetDestructionTimer(destructionTimer);
            }
            sprites.Add(spriteId,sprite);
            spritesByClass[sprite.Class].Add(sprite);
        }

        private void createInitialWorld(Client client)
        {
            //clear GameWorld
            sprites.Clear();
            spritesByClass[0].Clear();
            spritesByClass[1].Clear();
            spritesByClass[2].Clear();
            spritesByClass[3].Clear();
            spritesByClass[4].Clear();
            spritesByClass[5].Clear();
            score = 0;
            lives = 3;
            
            int wh = windowHeight / 10;
            int ww = windowWidth / 10;
            GameRandom r = GameRandom.getInstance();
            List<Byte[]> spriteData = new List<byte[]>();

            spriteData.Add(createSpriteData(0, windowWidth / 2, windowHeight / 4 * 3, 0, 0, 1, 0, 0));
            //spriteData.Add(createSpriteData(1, windowWidth / 2, windowHeight / 4 * 1, -MathHelper.Pi, 100, 1, 0, 0));
            spriteData.Add(createSpriteData(1, r.getInteger(ww * 1, ww * 4), r.getInteger(wh * 1, wh * 4), r.getSingle(-MathHelper.Pi, MathHelper.Pi), r.getSingle(0, 100), 4000, r.getSingle(-100, 100), 0));
            spriteData.Add(createSpriteData(1, r.getInteger(ww * 6, ww * 9), r.getInteger(wh * 1, wh * 4), r.getSingle(-MathHelper.Pi, MathHelper.Pi), r.getSingle(0, 100), 4000, r.getSingle(-100, 100), 0));
            spriteData.Add(createSpriteData(1, r.getInteger(ww * 1, ww * 4), r.getInteger(wh * 6, wh * 9), r.getSingle(-MathHelper.Pi, MathHelper.Pi), r.getSingle(0, 100), 4000, r.getSingle(-100, 100), 0));
            spriteData.Add(createSpriteData(1, r.getInteger(ww * 6, ww * 9), r.getInteger(wh * 6, wh * 9), r.getSingle(-MathHelper.Pi, MathHelper.Pi), r.getSingle(0, 100), 4000, r.getSingle(-100, 100), 0));
            
            byte[] payload = new byte[6 + (spriteData.Count * 32)];
            Buffer.BlockCopy(System.Text.Encoding.ASCII.GetBytes("CREA"), 0, payload, 0, 4);
            Buffer.BlockCopy(BitConverter.GetBytes((short)spriteData.Count), 0, payload, 4, 2);
            for (int i = 0; i < spriteData.Count; i++)
            {
                Buffer.BlockCopy(spriteData[i], 0, payload, 6 + i * 32, 32);
            }
            sendPacket(payload, client);
        }

        private void createObject(byte[] data)
        {
            byte[] payload = new byte[38];
            Buffer.BlockCopy(System.Text.Encoding.ASCII.GetBytes("CREA"), 0, payload, 0, 4);
            Buffer.BlockCopy(BitConverter.GetBytes((short)1), 0, payload, 4, 2);
            Buffer.BlockCopy(data, 0, payload, 6, 32);
            foreach (KeyValuePair<IPEndPoint, Client> client in clients)
            {
                sendPacket(payload, client.Value);
            }
        }

        private bool sendDestroyObject(int id)
        {
            if (!deleteingSprites.Contains(id))
            {
                deleteingSprites.Add(id);
                byte[] payload = new byte[8];
                Buffer.BlockCopy(System.Text.Encoding.ASCII.GetBytes("DROP"), 0, payload, 0, 4);
                Buffer.BlockCopy(BitConverter.GetBytes(id), 0, payload, 4, 4);
                foreach (KeyValuePair<IPEndPoint, Client> client in clients)
                {
                    sendPacket(payload, client.Value);
                }
                return true;
            }
            else 
            {
                return false;
            }
        }

        private byte[] createSpriteData(int textureId, float x, float y, float heading, float speed, float mass, float rotationSpeed, float duration)
        {
            byte[] data = new byte[32];
            Buffer.BlockCopy(BitConverter.GetBytes(textureId),0,data,0,4);
            Buffer.BlockCopy(BitConverter.GetBytes(x), 0, data, 4, 4);
            Buffer.BlockCopy(BitConverter.GetBytes(y), 0, data, 8, 4);
            Buffer.BlockCopy(BitConverter.GetBytes(heading), 0, data, 12, 4);
            Buffer.BlockCopy(BitConverter.GetBytes(speed), 0, data, 16, 4);
            Buffer.BlockCopy(BitConverter.GetBytes(mass), 0, data, 20, 4);
            Buffer.BlockCopy(BitConverter.GetBytes(rotationSpeed), 0, data, 24, 4);
            Buffer.BlockCopy(BitConverter.GetBytes(duration), 0, data, 28, 4);
            return data;
        }

        private void sendPacket(byte[] payload, Client client)
        {

            byte[] packet = new byte[UDP_HEADER_BYTE_LENGTH + payload.Length];
            Buffer.BlockCopy(System.Text.Encoding.ASCII.GetBytes("GAME"), 0, packet, 0, 4);
            Buffer.BlockCopy(BitConverter.GetBytes(client.LocalSequenceNumber++), 0, packet, 4, 4);
            Buffer.BlockCopy(payload, 0, packet, 8, payload.Length);
            server.Send(packet, packet.Length, client.Address);
        }
        
        /// <summary>
        /// Checks for and disconnects clients who have timeout
        /// </summary>
        /// <param name="time">Current time in ticks (from DateTime.Now.Ticks)</param>
        /// <param name="timeout">Timeout in ticks</param>
        private void timeoutClients(long time, long timeout)
        {
            // create list to hold clients who have reached timeout
            //List<IPEndPoint> deletelist = new List<IPEndPoint>();

            // check each connected clients for timeout
            for (int i = clients.Count - 1; i >= 0; i--)
            {
                if (time - clients.Values.ElementAt(i).LastPacketReceived > timeout)
                {
                    printLine("Client Disconnected: " + clients.Keys.ElementAt(i).ToString());
                    clients.Remove(clients.Keys.ElementAt(i));
                }

            }
        }
        private void printWelcome()
        {
            Console.BackgroundColor = BACKGROUND;
            Console.ForegroundColor = FOREGROUND;
            Console.Clear();
            printLine("Asteroidz Server");
            printLine("----------------");
        }

        private void printLine(string line)
        {
            Console.CursorLeft = 0;
            Console.WriteLine(line);
            Console.Write(PROMPT);
        }
        #endregion
    }
}
